package uk.co.q3c.deplan.server.dao;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import uk.co.q3c.deplan.client.domain.DomainObject;
import uk.co.q3c.deplan.client.domain.dag.ChangeInvalidException;
import uk.co.q3c.deplan.client.domain.dag.CycleDetectedException;
import uk.co.q3c.deplan.client.domain.dag.SharedStructure;
import uk.co.q3c.deplan.client.domain.dag.SharedStructureChangeLog;
import uk.co.q3c.deplan.client.domain.resource.PersistenceReferenceException;
import uk.co.q3c.deplan.server.admin.ServerServiceFactory;
import uk.co.q3c.deplan.server.service.CalendarService_BigT;
import uk.co.q3c.deplan.server.service.DesignService_BigT;
import uk.co.q3c.deplan.server.service.ResourceService_BigT;
import uk.co.q3c.deplan.server.service.TaskService_BigT;

import com.google.appengine.api.datastore.QueryResultIterable;
import com.google.appengine.api.datastore.QueryResultIterator;
import com.google.appengine.repackaged.com.google.common.collect.Lists;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.NotFoundException;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.Query;

/**
 * A base class for detail Dao's, this class provides some important features to hide some of the nuances of persistence from the higher
 * level code. Methods are provided to support put, get count, delete and save. Each of these will accept a query, or a class reference.
 * Each of these may return a single Object or a List, and a "discarded" feature is supported. A discarded object is simply marked as
 * discarded but kept in the datastore - analogous to putting it in the recycle bin, but being able to retrieve it later.<br>
 * <br>
 * Finally, all of these call an associated execute or get method - the main purpose of which is to handle datastore exceptions
 * 
 * 
 * 
 * @see TaskService_BigT
 * @see ResourceService_BigT
 * @see DesignService_BigT
 * @see CalendarService_BigT
 * @see AdminDao_BigT
 */
public class DomainDao_BigT implements DomainDao {
	private static final int MAX_RETRIES = 100;
	transient protected final Logger logger = Logger.getLogger(getClass().getName());
	private final ServerServiceFactory connection;

	public DomainDao_BigT(ServerServiceFactory connection) {
		super();
		this.connection = connection;

		// Query<DAG> q = ofy().query(DAG.class);
		// dependenciesKey = q.getKey();
		// dependenciesKey = ofy().getFactory().getKey(keyOrEntity)
	}

	protected Objectify ofy() {
		return connection.ofy();
	}

	@Override
	public <T extends DomainObject> Query<T> newQuery(Class<T> clazz) {
		return ofy().query(clazz);
	}

	// ================================================== count
	// =======================================================
	// -------------------------------------------------count, class only
	// ----------------------------------------------

	/**
	 * Counts all regardless of discarded state
	 * 
	 * @param clazz
	 * @return
	 */
	@Override
	public int countAll(Class<? extends DomainObject> clazz) {
		Query<? extends DomainObject> query = ofy().query(clazz);
		return executeCount(query);
	}

	@Override
	public int countUndiscarded(Class<? extends DomainObject> clazz) {
		Query<? extends DomainObject> query = ofy().query(clazz)
				.filter("discarded", false);
		int c = executeCount(query);
		return c;

	}

	@Override
	public int countDiscarded(Class<? extends DomainObject> clazz) {
		Query<? extends DomainObject> query = ofy().query(clazz)
				.filter("discarded", true);
		int c = executeCount(query);
		return c;

	}

	// -------------------------------------------------count, query supplied
	// -------------------------------------------
	/**
	 * Counts all regardless of discarded state
	 * 
	 * @param clazz
	 * @return
	 */
	protected <T extends DomainObject> int countAll(Query<T> query) {
		return executeCount(query);
	}

	protected <T extends DomainObject> int countUndiscarded(Query<T> query) {
		query.filter("discarded", false);
		return executeCount(query);
	}

	protected <T extends DomainObject> int countDiscarded(Query<T> query) {
		query.filter("discarded", true);
		return executeCount(query);
	}

	// =================================== find
	// ============================================================
	// ---------------------------------------- find List, class only
	// -----------------------------------------------------
	@Override
	public <T extends DomainObject> List<T> findAllList(Class<T> clazz) {
		Query<T> query = ofy().query(clazz);
		return executeFindList(query);
	}

	@Override
	public <T extends DomainObject> List<T> findUndiscardedList(Class<T> clazz) {
		Query<T> query = ofy().query(clazz)
				.filter("discarded", false);
		return executeFindList(query);
	}

	@Override
	public <T extends DomainObject> List<T> findDiscardedList(Class<T> clazz) {
		Query<T> query = ofy().query(clazz)
				.filter("discarded", true);
		return executeFindList(query);
	}

	// ---------------------------------------- find List from Query
	// -----------------------------------------------------

	protected <T extends DomainObject> List<T> findAllList(Query<T> query) {
		return executeFindList(query);
	}

	public <T extends DomainObject> List<T> findUndiscardedList(Query<T> query) {
		query.filter("discarded", false);
		return executeFindList(query);
	}

	protected <T extends DomainObject> List<T> findDiscardedList(Query<T> query) {
		query.filter("discarded", true);
		return executeFindList(query);
	}

	// ---------------------------------------- find first from Query
	// -----------------------------------------------------

	protected <T extends DomainObject> T findFirstFromAll(Query<T> query) {
		List<T> results = executeFindList(query);
		if (results == null) {
			return null;
		} else {
			return results.get(0);
		}
	}

	protected <T extends DomainObject> T findFirstFromUndiscarded(Query<T> query) {
		query.filter("discarded", false);
		List<T> results = executeFindList(query);
		if (results == null) {
			return null;
		} else {
			return results.get(0);
		}
	}

	protected <T extends DomainObject> T findFirstFromDiscarded(Query<T> query) {
		query.filter("discarded", true);
		List<T> results = executeFindList(query);
		if (results == null) {
			return null;
		} else {
			return results.get(0);
		}
	}

	// ---------------------------------------- find List from List of classes,
	// simple condition ---------------------------
	@Override
	public List<? extends DomainObject> findAllList(List<Class<? extends DomainObject>> classes, String condition, Object value) {
		List<DomainObject> results = new ArrayList<DomainObject>();
		for (Class<? extends DomainObject> clazz : classes) {
			Query<? extends DomainObject> query = ofy().query(clazz)
					.filter(condition, value);
			List<? extends DomainObject> q = findAllList(query);
			if (q != null) {
				results.addAll(q);
			}
		}
		return results;
	}

	@Override
	public List<? extends DomainObject> findUndiscardedList(List<Class<? extends DomainObject>> classes, String condition, Object value) {
		List<DomainObject> results = new ArrayList<DomainObject>();
		for (Class<? extends DomainObject> clazz : classes) {
			Query<? extends DomainObject> query = ofy().query(clazz);
			List<? extends DomainObject> interimResults = findUndiscardedList(query);
			if (interimResults != null) {
				results.addAll(interimResults);
			}
		}
		return results;
	}

	@Override
	public List<? extends DomainObject> findDiscardedList(List<Class<? extends DomainObject>> classes, String condition, Object value) {
		List<DomainObject> results = new ArrayList<DomainObject>();
		for (Class<? extends DomainObject> clazz : classes) {
			Query<? extends DomainObject> query = ofy().query(clazz);
			results.addAll(findDiscardedList(query));
		}
		return results;
	}

	// ---------------------------------------- find first from List of classes,
	// simple condition ---------------------------
	@Override
	public DomainObject findFirstFromAll(List<Class<? extends DomainObject>> classes, String condition, Object value) {
		List<? extends DomainObject> results = findAllList(classes, condition, value);
		if (results == null) {
			return null;
		} else {
			return results.get(0);
		}
	}

	@Override
	public DomainObject findFirstFromUndiscarded(List<Class<? extends DomainObject>> classes, String condition, Object value) {
		List<? extends DomainObject> results = findUndiscardedList(classes, condition, value);
		if (results == null) {
			return null;
		} else {
			return results.get(0);
		}
	}

	@Override
	public DomainObject findFirstFromDiscarded(List<Class<? extends DomainObject>> classes, String condition, Object value) {
		List<? extends DomainObject> results = findDiscardedList(classes, condition, value);
		if (results == null) {
			return null;
		} else {
			return results.get(0);
		}
	}

	// ------------------------------------------- find Keys from Query
	// ----------------------------------------------------
	protected <T extends DomainObject> List<Key<T>> findAllKeysList(Query<T> query) {
		return executeFindKeysList(query);
	}

	protected <T extends DomainObject> List<Key<T>> findUndiscardedKeysList(Query<T> query) {
		query.filter("discarded", false);
		return executeFindKeysList(query);
	}

	protected <T extends DomainObject> List<Key<T>> findDiscardedKeysList(Query<T> query) {
		query.filter("discarded", true);
		return executeFindKeysList(query);
	}

	// =============================================== delete
	// ============================================================
	// -----------------------------------------------delete class
	// ------------------------------------------------------
	@Override
	public void deleteAll(Class<? extends DomainObject> clazz) {
		Query<? extends DomainObject> query = ofy().query(clazz);
		QueryResultIterable<?> results = executeFetchKeys(query);
		executeDelete(results);
	}

	@Override
	public void deleteDiscarded(Class<? extends DomainObject> clazz) {
		Query<? extends DomainObject> query = ofy().query(clazz)
				.filter("discarded", true);
		QueryResultIterable<?> results = executeFetchKeys(query);
		executeDelete(results);
	}

	@Override
	public void deleteUndiscarded(Class<? extends DomainObject> clazz) {
		Query<? extends DomainObject> query = ofy().query(clazz)
				.filter("discarded", false);
		QueryResultIterable<?> results = executeFetchKeys(query);
		executeDelete(results);
	}

	// --------------------------------------------- delete from query
	// --------------------------------------------------------
	protected void deleteAll(Query<? extends DomainObject> query) {
		QueryResultIterable<?> results = executeFetchKeys(query);
		executeDelete(results);
	}

	protected void deleteDiscarded(Query<? extends DomainObject> query) {
		query.filter("discarded", true);
		QueryResultIterable<?> results = executeFetchKeys(query);
		executeDelete(results);
	}

	protected void deleteUndiscarded(Query<? extends DomainObject> query) {
		query.filter("discarded", false);
		QueryResultIterable<?> results = executeFetchKeys(query);
		executeDelete(results);
	}

	// ====================== special cases =================

	// ============================================== get
	// ===============================================================
	// ------------------------------- these need to run a post load as they
	// load objects---------------------------
	@Override
	public <T extends Object> T getObjectFromKey(Key<T> key) {
		if (key == null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Requested entity with null key ");
			}

			return null;
		}
		try {

			T result = ofy().get(key);
			if (result instanceof DomainObject) {
				DomainObject dob = (DomainObject) result;
				dob.afterLoad(connection);
			}
			return result;
		} catch (NotFoundException e) {
			if (logger.isDebugEnabled()) {
				logger.debug("Entity not found for key " + key.toString());
			}
			return null;
		}
	}

	@Override
	public <T extends DomainObject> Map<Key<T>, T> getMapOf(List<Key<T>> keys) {
		Map<Key<T>, T> result = ofy().get(keys);
		for (T r : result.values()) {
			r.afterLoad(connection);
		}
		return result;
	}

	@Override
	public <T extends DomainObject> List<T> getListOf(List<Key<? extends T>> keys) {
		Map<Key<T>, T> result = ofy().get(keys);
		for (T r : result.values()) {
			r.afterLoad(connection);
		}
		return Lists.newArrayList(result.values());
	}

	@Override
	public <T extends DomainObject> List<T> getList(List<Key<T>> keys) {
		Map<Key<T>, T> results = ofy().get(keys);
		for (T r : results.values()) {
			r.afterLoad(connection);
		}
		return Lists.newArrayList(results.values());
	}

	@Override
	public Map<Key<DomainObject>, DomainObject> getMap(List<Key<? extends DomainObject>> keys) {
		Map<Key<DomainObject>, DomainObject> results = ofy().get(keys);
		for (DomainObject r : results.values()) {
			r.afterLoad(connection);
		}
		return results;
	}

	// ================================================== execute
	// ===================================================
	// ------------------------------- these need to run a post load as they
	// load objects---------------------------

	protected <T extends DomainObject> List<T> executeFindList(Query<T> query) {
		logger.debug("Executing query " + query);
		QueryResultIterator<T> results = query.iterator();
		return doPostLoad(results);

	}

	// ----------------------------- these don't need a post load, either
	// counting, fetching keys, deleting or saving -------
	@Override
	public void save(DomainObject dob) {
		ofy().put(dob);
	}

	@Override
	public int executeCount(Query<? extends DomainObject> query) {
		logger.debug("Executing count query " + query);
		int c = query.countAll();
		if (logger.isDebugEnabled()) {
			logger.debug("Return count using " + query.toString() + " result = " + c);
		}
		return c;
	}

	protected QueryResultIterable<?> executeFetchKeys(Query<? extends DomainObject> query) {
		logger.debug("Executing fetch keys query " + query);
		return query.fetchKeys();
	}

	protected <T extends DomainObject> List<Key<T>> executeFindKeysList(Query<T> query) {
		logger.debug("Executing fetch keys query " + query);
		List<Key<T>> results = query.listKeys();
		return results;
	}

	protected void executeDelete(DomainObject domainObject) {
		ofy().delete(domainObject);
	}

	protected void executeDelete(Iterable<?> domainObjects) {
		ofy().delete(domainObjects);
	}

	// ================================================= other
	// =======================================================
	/**
	 * Sets {@link #ofy} to null. It will be recreated next time {@link #ofy()} is called
	 */
	@Override
	public void closeSession() {
		connection.closeSession();

	}

	/**
	 * @see DomainDao#keyFor(DomainObject)
	 * 
	 * @param <T>
	 * @param domainObject
	 * @return
	 */
	@Override
	public <T extends DomainObject> Key<T> keyFor(T domainObject) {
		if (domainObject == null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Cannot obtain key for a null object");
			}
			return null;
		}
		if (domainObject.getKeyId() == null) {
			if (logger.isEnabledFor(Level.WARN)) {
				logger.warn("Cannot obtain key for " + domainObject + " until it has been saved");
			}
			return null;
		}
		return ofy().getFactory()
				.getKey(domainObject);

	}

	@Override
	public void discard(DomainObject dob) throws CycleDetectedException, ChangeInvalidException, PersistenceReferenceException {
		dob.setDiscarded(true);
		save(dob);
	}

	private <T extends DomainObject> List<T> doPostLoad(QueryResultIterator<T> results) {
		if (!results.hasNext()) {
			return null;
		}
		List<T> list = Lists.newArrayList(results);
		for (T dob : list) {
			dob.afterLoad(connection);
		}
		return list;
	}

	@Override
	public SharedStructure saveShared(SharedStructure inMemoryInstance, Key<? extends SharedStructure> storedInstanceKey,
			SharedStructureChangeLog changeLog) {
		SharedStructure instanceToTry = inMemoryInstance;
		for (int i = 1; i <= MAX_RETRIES; i++) {
			// try and save the structure
			if (attemptSave(instanceToTry, storedInstanceKey)) {
				instanceToTry.clearChanges();
				break;
			} else {
				// but that could fail for all the usual reasons,
				// plus the chance that the stored version has changed while this one has been in memory
				// in any failure case, the changes which were applied to the in memory version must be applied to the more recent stored
				// version before attempting another save. Each individual change may also fail, and this is recorded in the change entry.
				// retrieve the latest version
				Objectify ofy = ObjectifyService.begin();
				SharedStructure latestInstance = ofy.get(storedInstanceKey);
				latestInstance.setChangeLog(changeLog);
				latestInstance.applyChanges();
				instanceToTry = latestInstance;
			}
		}
		return instanceToTry;

	}

	private boolean attemptSave(SharedStructure inMemoryInstance, Key<? extends SharedStructure> storedInstanceKey) {
		boolean success = false;
		Objectify ofyTxn = ObjectifyService.beginTransaction();
		try {
			// is the version we are trying to save the same as the one in the store?
			SharedStructure storedInstance = ofyTxn.get(storedInstanceKey);
			if (inMemoryInstance.getVersion() == storedInstance.getVersion()) {
				// if yes, up the version and try and save it, but in the event of failure (exception catch) rollback and return failed flag
				inMemoryInstance.incrementVersion();
				ofyTxn.put(inMemoryInstance);
				ofyTxn.getTxn()
						.commit();
				success = true;
			} else {
				// if not, roll back transaction and return failure
			}

		} catch (Exception e) {
			success = false;

		} finally {
			if (ofyTxn.getTxn()
					.isActive()) {
				ofyTxn.getTxn()
						.rollback();
			}
		}
		return success;
	}

}
